package zcrontab

import (
	"context"
	"fmt"
	"sort"
	"sync"
	"time"
)

type Config struct {
}

type Crontab struct {
	config Config

	wg  sync.WaitGroup
	ctx context.Context

	stop  chan struct{}
	tasks []*Task
}

type Task struct {
	ID       int64
	Schedule Schedule // 执行计划
	Name     string
	Next     time.Time
	Prev     time.Time
	Func     Function
}

type Function func()

type Handler struct {
	Name string
	Spec string
	Func Function
}

func New(ctx context.Context, config Config) *Crontab {
	return &Crontab{
		ctx:    ctx,
		config: config,
		stop:   make(chan struct{}),
		tasks:  make([]*Task, 0),
	}
}

func (c *Crontab) AddHandler(handlers []Handler) *Crontab {
	for _, handler := range handlers {
		schedule, err := Parse(handler.Spec)
		if err != nil {
			panic(fmt.Sprintf("crontab:添加计划任务失败[%s]", err.Error()))
		}

		c.tasks = append(c.tasks, &Task{
			ID:       0,
			Schedule: schedule,
			Name:     handler.Name,
			Next:     time.Time{},
			Prev:     time.Time{},
			Func:     handler.Func,
		})
	}
	return c
}

// byTime is a wrapper for sorting the entry array by time
// (with zero time at the end).
type byTime []*Task

func (s byTime) Len() int      { return len(s) }
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byTime) Less(i, j int) bool {
	// Two zero times should return false.
	// Otherwise, zero is "greater" than any other time.
	// (To sort it at the end of the list.)
	if s[i].Next.IsZero() {
		return false
	}
	if s[j].Next.IsZero() {
		return true
	}
	return s[i].Next.Before(s[j].Next)
}

// now returns current time in c location
func (c *Crontab) now() time.Time {
	return time.Now().In(time.Local)
}

func (c *Crontab) Start() {
	now := c.now()

	for _, task := range c.tasks {
		task.Next = task.Schedule.Next(now)
	}

	for {
		sort.Sort(byTime(c.tasks))

		var timer *time.Timer
		if len(c.tasks) == 0 || c.tasks[0].Next.IsZero() {
			// If there are no entries yet, just sleep - it still handles new entries
			// and stop requests.
			timer = time.NewTimer(100000 * time.Hour)
		} else {
			timer = time.NewTimer(c.tasks[0].Next.Sub(now))
		}

		for {
			select {
			case now = <-timer.C:
				now = now.In(time.Local)

				// Run every entry whose next time was less than now
				for _, task := range c.tasks {
					if task.Next.After(now) || task.Next.IsZero() {
						break
					}
					c.startJob(task.Func)
					task.Prev = task.Next
					task.Next = task.Schedule.Next(now)
				}

			case <-c.stop:
				timer.Stop()
				return
			}

			break
		}
	}
}

func (c *Crontab) Stop() {
	c.stop <- struct{}{}
	c.wg.Wait()
}

// startJob runs the given job in a new goroutine.
func (c *Crontab) startJob(fun Function) {
	c.wg.Add(1)
	go func() {
		defer c.wg.Done()
		fun()
	}()
}
